package net.sourceforge.openforecast;

import java.util.Collection;
import java.util.Map;
import java.util.TreeMap;

/** Represents a single observation or data point, consisting of one value of a dependent variable, and one or more
 * values of independent variables. Note that an Observation can refer to a previously observed data value, or a future
 * forecast value (an expected Observation).
 * 
 * @author Steven R. Gould
 * @since 0.3 */
public class Observation implements DataPoint {
	/** Stores the dependent value for this observation. */
	private double value;

	/** A collection of name-value pairs representing the independent variables associated with the current
	 * dependentValue. In each name-value pair, the name is a string representation of the name of the independent
	 * variable, and the value is its value corresponding to the current value of the dependent variable. */
	private final Map<String, Double> values = new TreeMap<>();

	/** Initializes the current Observation with the given value of the dependent variable. Note that this is provided
	 * as somewhat of a convenience - it will be necessary to call setIndependentValue for each independent variable
	 * name and value associated with this data point
	 * 
	 * @param value the initial value of the dependent variable. */
	public Observation(double value) {
		this.value = value;
	}

	/** Like a copy constructor, but constructs a new Observation object by making a copy of the values from the given
	 * data point.
	 * 
	 * @param dp the data point that is to be copied. */
	public Observation(DataPoint dp) {
		this.value = dp.getValue();

		for (String name : dp.getNames()) {
			values.put(name, dp.getValue(name));
		}
	}

	/** Returns the current value assigned to the dependent variable. This value can be changed by calling
	 * setDependentValue.
	 * 
	 * @return the current value of the dependent variable. */
	@Override
	public double getValue() {
		return value;
	}

	/** Sets the dependent variables' value to the given value.
	 * 
	 * @param value the new value for the dependent variable. */
	@Override
	public void setValue(double value) {
		this.value = value;
	}

	/** Returns the current value assigned to the named independent variable. This value can be changed by calling
	 * setIndependentValue.
	 * 
	 * @param name the name of the independent variable required.
	 * @return the current value of the named independent variable. */
	@Override
	public double getValue(String name) {
		Double value = values.get(name);
		if (null == value)
			throw new IllegalArgumentException("The independent variable " + name
					+ " is not defined for the current Observation. The current Observation is as follows: "
					+ toString());

		return value.doubleValue();
	}

	/** Sets the named independent variables' value to the given value. Each Observation can have one or more name-value
	 * pairs that represent the independent variables and their associated values.
	 * 
	 * @param value the new value for the dependent variable. */
	@Override
	public void setValue(String name, double value) {
		values.put(name, Double.valueOf(value));
	}

	/** Returns an array of all independent variable names. No checks are made to ensure that the names are unique.
	 * Rather, the names are extracted directly from the names used in defining and initializing the Observation.
	 * 
	 * @return an array of independent variable names for this Observation. */
	@Override
	public Collection<String> getNames() {
		return values.keySet();
	}

	/** Compares the given DataPoint to the current DataPoint/Observation, and returns true if, and only if, the two
	 * data points represent the same data point. That is, the dependent value matches and all the independent values
	 * match.
	 * 
	 * @param dp the DataPoint to compare this DataPoint/Observation object to.
	 * @return true if the given DataPoint represents the same data point as this DataPoint/Observation object. */
	@Override
	public boolean equals(DataPoint dp) {
		if (value != dp.getValue())
			return false;

		if (getNames().size() != dp.getNames()
			.size())
			return false;

		for (Map.Entry<String, Double> entry : values.entrySet()) {
			double val = dp.getValue(entry.getKey());
			if (entry.getValue() != val)
				return false;
		}

		// All variable values match, so the given DataPoint represents the
		// same data point as this Observation
		return true;
	}

	/** Overrides the default toString method to provide a more meaningful output of an Observation. Each independent
	 * variable is listed with its value, then the dependent variable and its value are listed.
	 * 
	 * @return a string representation of this Observation. */
	@Override
	public String toString() {
		StringBuffer sb = new StringBuffer();

		sb.append('(')
			.append(value);
		values.forEach((k, v) -> {
			sb.append(',')
				.append(k)
				.append('=')
				.append(v);
		});
		sb.append(')');

		return sb.toString();
	}
}